// app/api/data-room/[projectId]/download-multiple/route.ts import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth/next'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { FileService, type FileAccessContext } from '@/lib/services/fileService'; import { promises as fs } from 'fs'; import path from 'path'; import archiver from 'archiver'; import { Readable } from 'stream'; import db from "@/db/db"; import { fileItems } from "@/db/schema/fileSystem"; import { eq, inArray } from "drizzle-orm"; export async function POST( request: NextRequest, { params }: { params: { projectId: string } } ) { try { const session = await getServerSession(authOptions); if (!session?.user) { return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 }); } const body = await request.json(); const { fileIds } = body; if (!fileIds || !Array.isArray(fileIds) || fileIds.length === 0) { return NextResponse.json( { error: '파일 ID가 제공되지 않았습니다' }, { status: 400 } ); } // 너무 많은 파일 방지 (최대 100개) if (fileIds.length > 100) { return NextResponse.json( { error: '한 번에 최대 100개의 파일만 다운로드할 수 있습니다' }, { status: 400 } ); } const context: FileAccessContext = { userId: Number(session.user.id), userDomain: session.user.domain || 'partners', userEmail: session.user.email, ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined, userAgent: request.headers.get('user-agent') || undefined, }; const fileService = new FileService(); const downloadableFiles: Array<{ file: any; absolutePath: string; }> = []; // 각 파일의 접근 권한 확인 및 경로 확인 for (const fileId of fileIds) { // 권한 확인 const hasAccess = await fileService.checkFileAccess( fileId, context, 'download' ); if (!hasAccess) { console.warn(`파일 ${fileId}에 대한 다운로드 권한이 없습니다`); continue; } // 파일 정보 가져오기 const file = await db.query.fileItems.findFirst({ where: eq(fileItems.id, fileId), }); if (!file || !file.filePath || file.type !== 'file') { console.warn(`파일 ${fileId}를 찾을 수 없거나 폴더입니다`); continue; } // 실제 파일 경로 구성 const nasPath = process.env.NAS_PATH || "/evcp_nas"; const isProduction = process.env.NODE_ENV === "production"; let absolutePath: string; if (isProduction) { const relativePath = file.filePath.replace('/api/files/', ''); absolutePath = path.join(nasPath, relativePath); } else { absolutePath = path.join(process.cwd(), 'public', file.filePath); } // 파일 존재 여부 확인 try { await fs.access(absolutePath); downloadableFiles.push({ file, absolutePath }); // 다운로드 카운트 증가 및 로그 기록 await fileService.downloadFile(fileId, context); } catch (error) { console.warn(`파일 ${absolutePath}를 찾을 수 없습니다`); } } if (downloadableFiles.length === 0) { return NextResponse.json( { error: '다운로드 가능한 파일이 없습니다' }, { status: 404 } ); } // ZIP 스트림 생성 const archive = archiver('zip', { zlib: { level: 5 } // 압축 레벨 (1-9, 5가 균형적) }); // 스트림을 Response로 변환 const stream = new ReadableStream({ start(controller) { archive.on('data', (chunk) => controller.enqueue(chunk)); archive.on('end', () => controller.close()); archive.on('error', (err) => controller.error(err)); }, }); // 파일들을 ZIP에 추가 for (const { file, absolutePath } of downloadableFiles) { try { const fileBuffer = await fs.readFile(absolutePath); // 파일명 중복 방지를 위한 고유 이름 생성 const uniqueName = `${path.parse(file.name).name}_${file.id.slice(0, 8)}${path.extname(file.name)}`; archive.append(fileBuffer, { name: uniqueName }); } catch (error) { console.error(`파일 추가 실패: ${file.name}`, error); } } // ZIP 완료 archive.finalize(); // Response Headers 설정 const headers = new Headers(); headers.set('Content-Type', 'application/zip'); headers.set('Content-Disposition', 'attachment; filename="files.zip"'); headers.set('Cache-Control', 'no-cache, no-store, must-revalidate'); headers.set('Pragma', 'no-cache'); headers.set('Expires', '0'); return new NextResponse(stream, { status: 200, headers, }); } catch (error) { console.error('다중 파일 다운로드 오류:', error); return NextResponse.json( { error: '다중 파일 다운로드에 실패했습니다' }, { status: 500 } ); } }